/**********************************************************************
The MIT License (MIT)

Copyright (c) 2013 J.J.King

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

(http://opensource.org/licenses/MIT)
**********************************************************************/

// jjkingTableHelper - J.J.King's Table Helper, an Event Handler Library
//
// This free and open-source javascript library is currently hosted at:
//  https://code.google.com/p/tablehelper/
// See readme.txt on this site for general introduction.
//
// This javascript library and its sample html files are tested under:
//  - Safari 6.0.5 under Mac OS X 10.8.5
//  - Firefox 26.0 ditto
//  - jQuery (develpment began with version 1.10.2)
//  - Ben Alman's jQuery resize event (tested with version v1.1)
//
// History of development and changes: 
// 13/12/28 thead/tfoot/tbody table test
// 13/12/29 resize test
// 13/12/30 resize cell
// 13/12/31 adjust scroll
// 13/12/31 resize height
// 14/01/02 made immediately-invoked style function library
// 14/01/12 thead-tfoot-resize.html done
// 14/01/16 side-by-side-scroll.html done
// 14/01/25 no source and target distinction required for resizing
// 14/01/26 resizing with array of heights or width
// 14/02/01 options
// 14/02/08 error check on options
//
// TODO: colspan and rowspan handling

var jjkingTableHelper = (function ($) {
	
	// private variables and functions

	var debug = true;
	
	function log(s) {
		if (debug) console.log(s);
	}

	function error(s) {
		alert('tablehelper: ' + s);
	}

	function isElement(e) { return e.nodeType == 1; }
	function isJQuery(j) { return j instanceof jQuery; }
	function isArray(a) { return a instanceof Array; }
	
	/**
	 * Make sure the passed argument is an element; not an object nor array.
	 * Exracts the first single element from a jquery object or array of objects or elements.
	 */
	function toElement(obj) {
		
		if (isArray(obj) && obj.length > 0) {
		
			if (!isArray(obj[0])) return toElement(obj[0]); // take the first one	
			else return undefined;
		}
		else if (isElement(obj)) return obj;
		else if (isJQuery(obj)) return obj[0];
		else return undefined;
	}

	/**
	 * Convert array of objects into array of elements for easy handling
	 */
	function toElementArray(objs) {
		
		if (!isArray(objs)) { // allow single object
		
			var o = toElement(objs);
			if (o === undefined) return undefined;
			else return [ o ]; // make an array
		}
		
		if (objs.length == 0) return undefined;
		
		var a = new Array();
		for (var i = 0; i < objs.length; i++) a.push(toElement(objs[i]));
		return a;
	}
	
	/**
	 * Return an array of row heights greatest among one or more tables
	 */
	function maxHeightArray(tables) {
	
		// Note: you don't have to take 'rowspan' attribute into account
		// when you determine row height because rowspan appears in <td> level.
		
		var maxHeights = [];

		for (ti = 0; ti < tables.length; ti++) {
		
			var table = tables[ti];
			
			for (var ri = 0; ri < table.rows.length; ri++) {
			
				var height = $(table.rows[ri]).height();
				var prevHeight = maxHeights[ri];
				maxHeights[ri] = prevHeight === undefined ? height : Math.max(height, prevHeight);
			}
		}
		
		return maxHeights;
	}

	/**
	 * Array of max cell widths among the tables
	 */
	function maxWidthArray(tables) {
	
		var maxWidths = [];

		for (ti = 0; ti < tables.length; ti++) {
		
			var table = tables[ti];
			var cells = table.rows[0].cells; // first row
			
			for (var ci = 0; ci < cells.length; ci++) {
			
				var cell = $(cells[ci]);
				
				var colspan = cell.prop('colspan');
				if (colspan !== undefined && colspan > 1) {
				
					// TODO: colspan must be taken into account on building cell width array
					
					error('colspan not supported yet');
					return;
				}

				var rowspan = cell.prop('rowspan');
				if (rowspan !== undefined && rowspan > 1) {
				
					// TODO: rowspan is further difficult to handle since it affects handling of
					// multiple rows.
					
					error('rowspan not supported yet');
					return;
				}

				var width = cell.width();
				var prevWidth = maxWidths[ci];
				maxWidths[ci] = prevWidth === undefined ? width : Math.max(width, prevWidth);
			}
		}
		
		return maxWidths;
	}

	/**
	 * Set height of rows in one or more tables to those of the row heights array
	 */
	function resizeRowHeights(tables, rowHeights) {
	
		for (var ri = 0; ri < rowHeights.length; ri++) {
		
			var rowHeight = rowHeights[ri];
			
			for (ti = 0; ti < tables.length; ti++) {
			
				var table = tables[ti];
				if (debug) $(table.rows[ri]).css('color', 'yellow');
				table.rows[ri].height = rowHeight;
			}
		}
	}

	/**
	 * Set width of cells in one or more tables to those of cel widths array
	 */
	function resizeCellWidths(tables, cellWidths) {
	
		var tableWidth = 0; // test
		
		for (var ci = 0; ci < cellWidths.length; ci++) {
		
			var cellWidth = cellWidths[ci];
			tableWidth += cellWidth;
			
			for (ti = 0; ti < tables.length; ti++) {

				var table = tables[ti];
				var cell = $(table.rows[0].cells[ci]);
				
				var colspan = cell.prop('colspan');
				if (colspan !== undefined && colspan > 1) {
				
					// TODO: colspan must be taken into account on setting cell width 
					
					error('colspan not supported yet');
					return;
				}

				var rowspan = cell.prop('rowspan');
				if (rowspan !== undefined && rowspan > 1) {
				
					// TODO: rowspan is further difficult to handle since it affects handling of
					// multiple rows.
					
					error('rowspan not supported yet');
					return;
				}

				if (debug) cell.css('color', 'yellow');
				
				// cell.width(cellWidth);
				// cell.css('width', parseInt(cellWidth) + 'px');
				cell.width = cellWidth;
				
				// if (ci == cellWidths.length - 1) table.width = tableWidth; // test: try to set total width
			}
		}
	}

	/**
	 * build width array from the colgroup element; <col width="..."> checked
	 */
	function widthArrayFromColgroup(colgroup) { // colgroup must be an element
	
		var cols = $(colgroup).children();
		
		var widths = [];
		
		for (var ci = 0; ci < cols.length; ci++) {
		
			var col = $(cols[ci]);
			var span = col.prop('span');
			if (span > 1) {
			
				error('span attribute with more than one in col element');
				return null;
			}

			var width = col.prop('width');
			if (width === undefined) {
			
				error('width attribute missing in col element');
				return null;
			}
			
			widths[ci] = width;
		}
		
		return widths;
	}
	
	 // public entry points to event handlers and utility functions
	return {
		
		// four event handlers follow:
		// - interlocked scrolling vertically and horizontally
		// - adjustment of row height and cell width.
		// Note that resizie handlers depend on a resize plugin such as Alman's.
		
		/**
		 * Interlock vertical scrolling of one or more tables with that of a table. 
		 * 
		 * Source must be a single table element or section element (thead, tfoot or tbody).
		 * Targets must be an array of table and/or section elements (in jquery objects).
		 * The rows and/or cells of the table/section conpoments in the targets array
		 * can be selectively adjusted to those of the source table.
		 */
		scrolledVertically: function (e) {
			// log('scrolledVertically: ' + e.target.scrollTop);
			
			var source = toElement(e.target); // naked element
			if (source === undefined) {
			
				error('scrollVertically: invalid source');
				return;
			}
			
			var targets = toElementArray(e.data); // array of jquery objects or elements
			if (targets === undefined) {
			
				error('scrollVertically: invalid targets');
				return;
			}
			
			for (var i = 0; i < targets.length; i++) { // for each element
			
				$(targets[i]).scrollTop($(source).scrollTop());
			}
		},
		
		/**
		 * Interlock horizontal scrolling of one or more tables with that of a table. 
		 */
		scrolledHorizontally: function (e) {
			// log('scrolledHorizontally: ' + e.target.scrollLeft);
			
			var source = toElement(e.target);
			if (source === undefined) {
			
				error('scrollHorizntally: invalid source');
				return;
			}
			
			var targets = toElementArray(e.data);
			if (targets === undefined) {
			
				error('scrollHorizontally: invalid targets');
				return;
			}

			for (var i = 0; i < targets.length; i++) { // for each element
			
				$(targets[i]).scrollLeft($(source).scrollLeft());
			}
		},

		/**
		 * Link a table with one or more other tables for interlocked row height.
		 * These tables all will have the same row height, the greatest of all tables,
		 * regardless it is a source or targets.
		 */
		heightChanged: function (e) {
			log('heightChanged: target = ' + e.target + ', currentTarget = ' + e.currentTarget);
			
			// e.target is what triggers the event dispatcher to trigger and e.currentTarget is what you assigned your listener to.

			var source = toElement(e.currentTarget); // table or section where an event occurs in its inside
			if (source === undefined) {
			
				error('heightChanged: invalid source');
				return;
			}

			var targets; // tables or sections to be interlocked with the source
			var heights = null; // array of heights

			if (e.data === undefined) {
			
				error('heightChanged: invalid options');
				return;
			}
			else if (isArray(e.data)) { // short format: [ t1, t2, ... ];
			
				targets = toElementArray(e.data);
				if (targets === undefined) {
				
					error('heightChanged: invalid targets');
					return;
				}
			}
			else { // long format: { options... }
			
				targets = toElementArray(e.data.tables); // { tables: [ t1, t2, ... ] }
				if (targets === undefined) {
				
					error('heightChanged: invalid "tables" option');
					return;
				}
				
				if (e.data.heights !== undefined) { // { heights: [ h1, h2, ... ] }
				
					heights = e.data.heights;
					if (!isArray(heights)) {
					
						error('heightChanged: invalid "heights" options');
						return;
					}
				}
			}
			
			if (heights == null) heights = maxHeightArray(targets); // default max heights
			
			targets.unshift(source); // add source too
			resizeRowHeights(targets, heights);
		},
		
		/**
		 * Link a table with one or more other tables for interlocked cell width.
		 */
		widthChanged: function (e) {
			log('widthChanged: target = ' + e.target + ', currentTarget = ' + e.currentTarget);
			
			var source = toElement(e.currentTarget); // source table
			if (source === undefined) {
			
				error('widthChanged: invalid source');
				return;
			}

			var targets; // tables to be interlocked
			var widths = null; // array of width
			
			if (e.data === undefined) {
			
				error('widthChanged: invalid options');
				return;
			}
			else if (isArray(e.data)) { // short format
			
				targets = toElementArray(e.data);
				if (targets === undefined) {
				
					error('widthChanged: invalid targets');
					return;
				}
			}
			else { // long format: { options... }
			
				targets = toElementArray(e.data.tables); // { tables: [ t1, t2, ... ] }
				if (targets === undefined) {
				
					error('widthChanged: invalid "tables" option');
					return;
				}
				
				if (e.data.colgroup !== undefined) { // { colgroup: colgroup-element }
				
					var colgroup = toElement(e.data.colgroup);
					if (colgroup === undefined) {
						error('widthChanged: invalid "colgroup" option');
						return;
					}
					widths = widthArrayFromColgroup(colgroup);
				}
				else if (e.data.widths !== undefined) { // { widths: [ w1, w2, ... ] }
				
					widths = e.data.widths;
					if (!isArray(widths)) {
					
						error('widthChanged: invalid "widths" options');
						return;
					}
				}
			}
			
			if (widths == null) widths = maxWidthArray(targets); // default max widths
			
			targets.unshift(source);
			resizeCellWidths(targets, widths);
		}
		
	} // end of return
	
	// Note: I first wrote as:
	// return
	// {
	//	...
	// }
	// But both safari and firefox debuggers complain about something about the next line.
	// They do not after I put '{' on the same line as 'return'
	// So I now know that this notation is risky.
	
})(jQuery);
